using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using NFreeMarker.Log;
using NFreeMarker.Template;
using Environment = NFreeMarker.Core.Environment;

namespace NFreeMarker.Cache
{
    /**
     * A class that performs caching and on-demand loading of the templates.
     * The actual loading is delegated to a {@link TemplateLoader}. Also,
     * various constructors provide you with convenient caches with predefined
     * behavior. Typically you don't use this class directly - in normal
     * circumstances it is hidden behind a {@link Configuration}.
     * @author Attila Szegedi, szegedia at freemail dot hu
     * @version $Id: TemplateCache.java,v 1.62.2.2 2007/04/03 18:06:07 szegedia Exp $
     */
    public class TemplateCache
    {
        private const string Asteriskstr = "*";
        private const string LocaleSeparator = "-";
        private const char Asterisk = '*';
        private const string CurrentDirPathPrefix = "./";
        private const string CurrentDirPath = "/./";
        private const string ParentDirPathPrefix = "../";
        private const string ParentDirPath = "/../";
        private const char Slash = '/';
        private static readonly Logger Logger = Logger.GetLogger("freemarker.cache");

        private readonly ITemplateLoader _mainLoader;
        /** DD: [noFallback]
            , 
            fallbackTemplateLoader = new ClassTemplateLoader(TemplateCache.class, "/");
            */
        /** Here we keep our cached templates */
        private readonly ICacheStorage _storage;
        private readonly bool _isStorageConcurrent;
        /** The default refresh delay in milliseconds. */
        private long _delay = 5000;
        /** Specifies if localized template lookup is enabled or not */
        private bool _localizedLookup = true;

        private Configuration _config;

        /**
         * Returns a template cache that will first try to load a template from
         * the file system relative to the current user directory (i.e. the value
         * of the system property <code>user.dir</code>), then from the classpath.
         * This default template cache suits many applications.
         */
        public TemplateCache()
            : this(CreateDefaultTemplateLoader())
        {
        }

        private static ITemplateLoader CreateDefaultTemplateLoader()
        {
            try
            {
                return new FileTemplateLoader();
            }
            catch (Exception e)
            {
                Logger.Warn("Could not create a file template loader for current directory", e);
                return null;
            }
        }

        /**
         * Creates a new template cache with a custom template loader that is used
         * to load the templates.
         * @param loader the template loader to use.
         */
        public TemplateCache(ITemplateLoader loader)
            : this(loader, new SoftCacheStorage())
        {
        }

        /**
         * Creates a new template cache with a custom template loader that is used
         * to load the templates.
         * @param loader the template loader to use.
         */
        public TemplateCache(ITemplateLoader loader, ICacheStorage storage)
        {
            _mainLoader = loader;
            _storage = storage;
            if (storage == null)
            {
                throw new ArgumentException("storage == null");
            }
            _isStorageConcurrent = storage is IConcurrentCacheStorage &&
                                   ((IConcurrentCacheStorage) storage).IsConcurrent();
        }

        /**
         * Sets the configuration object to which this cache belongs. This
         * method is called by the configuration itself to establish the
         * relation, and should not be called by users.
         */
        public void SetConfiguration(Configuration config)
        {
            _config = config;
            Clear();
        }

        public ITemplateLoader GetTemplateLoader()
        {
            return _mainLoader;
        }

        public ICacheStorage GetCacheStorage()
        {
            return _storage;
        }

        /**
         * Loads a template with the given name, in the specified locale and
         * using the specified character encoding.
         *
         * @param name the name of the template. Can't be null. The exact syntax of the name
         * is interpreted by the underlying {@link TemplateLoader}, but the
         * cache makes some assumptions. First, the name is expected to be
         * a hierarchical path, with path components separated by a slash
         * character (not with backslash!). The path (the name) must <em>not</em> begin with slash;
         * the path is always relative to the "template root directory".
         * Then, the <tt>..</tt> and <tt>.</tt> path metaelements will be resolved.
         * For example, if the name is <tt>a/../b/./c.ftl</tt>, then it will be
         * simplified to <tt>b/c.ftl</tt>. The rules regarding this are same as with conventional
         * UN*X paths. The path must not reach outside the template root directory, that is,
         * it can't be something like <tt>"../templates/my.ftl"</tt> (not even if the pervious path
         * happens to be equivalent with <tt>"/my.ftl"</tt>).
         * Further, the path is allowed to contain at most
         * one path element whose name is <tt>*</tt> (asterisk). This path metaelement triggers the
         * <i>acquisition mechanism</i>. If the template is not found in
         * the location described by the concatenation of the path left to the
         * asterisk (called base path) and the part to the right of the asterisk
         * (called resource path), the cache will attempt to remove the rightmost
         * path component from the base path ("go up one directory") and concatenate
         * that with the resource path. The process is repeated until either a
         * template is found, or the base path is completely exhausted.
         *
         * @param locale the requested locale of the template. Can't be null.
         * Assuming you have specified <code>en_US</code> as the locale and
         * <code>myTemplate.html</code> as the name of the template, the cache will
         * first try to retrieve <code>myTemplate_en_US.html</code>, then
         * <code>myTemplate.html_en.html</code>, and finally
         * <code>myTemplate.html</code>.
         *
         * @param encoding the character encoding used to interpret the template
         * source bytes. Can't be null.
         *
         * @param parse if true, the loaded template is parsed and interpreted
         * as a regular FreeMarker template. If false, the loaded template is
         * treated as an unparsed block of text.
         *
         * @return the loaded template, or null if the template is not found.
         */
        public Template.Template GetTemplate(string name, CultureInfo locale, Encoding encoding, bool parse)
        {
            if (name == null) {
                throw new ArgumentException("Argument \"name\" can't be null");
            }
            if (locale == null) {
                throw new ArgumentException("Argument \"locale\" can't be null");
            }
            if (encoding == null) {
                throw new ArgumentException("Argument \"encoding\" can't be null");
            }
            name = NormalizeName(name);
            if(name == null) {
                return null;
            }
            Template.Template result = null;
            if (_mainLoader != null) {
                result = GetTemplate(_mainLoader, name, locale, encoding, parse);
            }
            /** DD: [noFallback]
            if (result == null && name.toLowerCase().endsWith(".ftl")) {
                result = getTemplate(fallbackTemplateLoader, name, locale, encoding, parse);
            }
            */
            return result;
        }    

        private Template.Template GetTemplate(ITemplateLoader loader, string name, CultureInfo locale, Encoding encoding, bool parse)
        {
            bool debug = Logger.IsDebugEnabled();
            string debugName = debug ? name + "[" + locale + "," + encoding + (parse ? ",parsed] " : ",unparsed] ") : null;
            var tk = new TemplateKey(name, locale, encoding, parse);

            CachedTemplate cachedTemplate;
            if (_isStorageConcurrent)
            {
                cachedTemplate = (CachedTemplate)_storage.Get(tk);
            }
            else
            {
                lock (_storage)
                {
                    cachedTemplate = (CachedTemplate)_storage.Get(tk);
                }
            }
            long now = DateTimeOffset.Now.ToFileTime();
            long lastModified = -1L;
            object newlyFoundSource = null;
            bool rethrown = false;
            try
            {
                if (cachedTemplate != null)
                {
                    // If we're within the refresh delay, return the cached copy
                    if (now - cachedTemplate.LastChecked < _delay)
                    {
                        if (debug)
                        {
                            Logger.Debug(debugName + "cached copy not yet stale; using cached.");
                        }
                        // Can be null, indicating a cached negative lookup
                        object t = cachedTemplate.TemplateOrException;
                        if (t is Template.Template || t == null)
                        {
                            return (Template.Template)t;
                        }
                        //else if (t is RuntimeException)
                        //{
                        //    ThrowLoadFailedException((RuntimeException)t);
                        //}
                        else if (t is IOException)
                        {
                            rethrown = true;
                            ThrowLoadFailedException((IOException)t);
                        }
                        //throw new RuntimeException("t is " + t.GetType().FullName);
                        throw new NotImplementedException();
                    }
                    // Clone as the instance bound to the map should be treated as
                    // immutable to ensure proper concurrent semantics
                    cachedTemplate = cachedTemplate.CloneCachedTemplate();
                    // Update the last-checked flag
                    cachedTemplate.LastChecked = now;

                    // Find the template source
                    newlyFoundSource = FindTemplateSource(name, locale);

                    // Template source was removed
                    if (newlyFoundSource == null)
                    {
                        if (debug)
                        {
                            Logger.Debug(debugName + "no source found.");
                        }
                        StoreNegativeLookup(tk, cachedTemplate, null);
                        return null;
                    }

                    // If the source didn't change and its last modified date
                    // also didn't change, return the cached version.
                    lastModified = loader.GetLastModified(newlyFoundSource);
                    bool lastModifiedNotChanged = lastModified == cachedTemplate.LastModified;
                    bool sourceEquals = newlyFoundSource.Equals(cachedTemplate.Source);
                    if (lastModifiedNotChanged && sourceEquals)
                    {
                        if (debug)
                        {
                            Logger.Debug(debugName + "using cached since " +
                               newlyFoundSource + " didn't change.");
                        }
                        StoreCached(tk, cachedTemplate);
                        return (Template.Template)cachedTemplate.TemplateOrException;
                    }
                    else
                    {
                        if (debug && !sourceEquals)
                        {
                            Logger.Debug("Updating source, info for cause: " +
                               "sourceEquals=" + sourceEquals +
                               ", newlyFoundSource=" + newlyFoundSource +
                               ", cachedTemplate.source=" + cachedTemplate.Source);
                        }
                        if (debug && !lastModifiedNotChanged)
                        {
                            Logger.Debug("Updating source, info for cause: " +
                               "lastModifiedNotChanged=" + lastModifiedNotChanged +
                               ", cache lastModified=" + cachedTemplate.LastModified +
                               " != file lastModified=" + lastModified);
                        }
                        // Update the source
                        cachedTemplate.Source = newlyFoundSource;
                    }
                }
                else
                {
                    if (debug)
                    {
                        Logger.Debug("Could not find template in cache, "
                           + "creating new one; id=[" +
                           tk.Name + "[" + tk.Locale + "," + tk.Encoding +
                           (tk.Parse ? ",parsed] " : ",unparsed] ") + "]");
                    }

                    // Construct a new CachedTemplate entry. Note we set the
                    // cachedTemplate.lastModified to Long.MIN_VALUE. This is
                    // a flag that signs it has to be explicitly queried later on.
                    cachedTemplate = new CachedTemplate();
                    cachedTemplate.LastChecked = now;
                    newlyFoundSource = FindTemplateSource(name, locale);
                    if (newlyFoundSource == null)
                    {
                        StoreNegativeLookup(tk, cachedTemplate, null);
                        return null;
                    }
                    cachedTemplate.Source = newlyFoundSource;
                    cachedTemplate.LastModified = lastModified = long.MinValue;
                }
                if (debug)
                {
                    Logger.Debug("Compiling FreeMarker template " +
                       debugName + " from " + newlyFoundSource);
                }
                // If we get here, then we need to (re)load the template
                object source = cachedTemplate.Source;
                Template.Template template = LoadTemplate(loader, name, locale, encoding, parse, source);
                cachedTemplate.TemplateOrException = template;
                cachedTemplate.LastModified =
                    lastModified == long.MinValue
                        ? loader.GetLastModified(source)
                        : lastModified;
                StoreCached(tk, cachedTemplate);
                return template;
            }
            // TODO: catch (RuntimeException e)
            //{
            //    StoreNegativeLookup(tk, cachedTemplate, e);
            //    throw e;
            //}
            catch (IOException e)
            {
                if (!rethrown)
                {
                    StoreNegativeLookup(tk, cachedTemplate, e);
                }
                throw;
            }
            finally
            {
                if (newlyFoundSource != null)
                {
                    loader.CloseTemplateSource(newlyFoundSource);
                }
            }
        }

        //TODO: private static readonly Method INIT_CAUSE = GetInitCauseMethod();

        //TODO: private static readonly Method GetInitCauseMethod() {
        //    try {
        //        return Throwable.class.getMethod("initCause", new Class[] { Throwable.class });
        //    } catch(NoSuchMethodException e) {
        //        return null;
        //    }
        //}

        private void ThrowLoadFailedException(Exception e)
        {
            //IOException ioe;
            //if(INIT_CAUSE != null) {
            //    ioe = new IOException("There was an error loading the " +
            //        "template on an earlier attempt; it is attached as a cause");
            //    try {
            //        INIT_CAUSE.invoke(ioe, new object[] { e });
            //    } catch(RuntimeException ex) {
            //        throw ex;
            //    } catch(Exception ex) {
            //        throw new UndeclaredThrowableException(ex);
            //    }
            //}
            //else {
            //    ioe = new IOException("There was an error loading the " +
            //    "template on an earlier attempt: " + e.GetType().FullName + 
            //    ": " + e.Message);
            //}
            //throw ioe;

            throw new NotImplementedException();
        }

        private void StoreNegativeLookup(TemplateKey tk,
                                         CachedTemplate cachedTemplate, Exception e)
        {
            cachedTemplate.TemplateOrException = e;
            cachedTemplate.Source = null;
            cachedTemplate.LastModified = 0L;
            StoreCached(tk, cachedTemplate);
        }

        private void StoreCached(TemplateKey tk, CachedTemplate cachedTemplate)
        {
            if (_isStorageConcurrent)
            {
                _storage.Put(tk, cachedTemplate);
            }
            else
            {
                lock (_storage)
                {
                    _storage.Put(tk, cachedTemplate);
                }
            }
        }

        private Template.Template LoadTemplate(ITemplateLoader loader, string name, CultureInfo locale, Encoding encoding,
                                       bool parse, object source)
        {
            Template.Template template;
            TextReader reader = loader.GetReader(source, encoding);
            try
            {
                if(parse)
                {
                    try {
                        template = new Template.Template(name, reader, _config, encoding);
                    }
                    catch (Template.Template.WrongEncodingException wee) {
                        encoding = wee.SpecifiedEncoding;
                        reader = loader.GetReader(source, encoding);
                        template = new Template.Template(name, reader, _config, encoding);
                    }
                    template.SetLocale(locale);
                }
                else
                {
                    // Read the contents into a StringWriter, then construct a single-textblock
                    // template from it.
                    var sw = new StringWriter();
                    var buf = new char[4096];
                    for(;;)
                    {
                        int charsRead = reader.Read(buf, 0, buf.Length);
                        if (charsRead > 0)
                        {
                            sw.Write(buf, 0, charsRead);
                        }
                        else if(charsRead == 0)
                        {
                            break;
                        }
                    }
                    template = Template.Template.GetPlainTextTemplate(name, sw.ToString(), _config);
                    template.SetLocale(locale);
                }
                template.SetEncoding(encoding);
            }
            finally
            {
                reader.Close();
            }
            return template;
        }

        /**
         * Gets the delay in milliseconds between checking for newer versions of a
         * template source.
         * @return the current value of the delay
         */
        [MethodImpl(MethodImplOptions.Synchronized)]
        public long GetDelay()
        {
            return _delay;
        }

        /**
         * Sets the delay in milliseconds between checking for newer versions of a
         * template sources.
         * @param delay the new value of the delay
         */
        [MethodImpl(MethodImplOptions.Synchronized)]
        public void SetDelay(long delay)
        {
            _delay = delay;
        }

        /**
         * Returns if localized template lookup is enabled or not.
         */
        [MethodImpl(MethodImplOptions.Synchronized)]
        public bool GetLocalizedLookup()
        {
            return _localizedLookup;
        }

        /**
         * Setis if localized template lookup is enabled or not.
         */
        [MethodImpl(MethodImplOptions.Synchronized)]
        public void SetLocalizedLookup(bool localizedLookup)
        {
            _localizedLookup = localizedLookup;
        }

        /**
         * Removes all entries from the cache, forcing reloading of templates
         * on subsequent {@link #getTemplate(string, Locale, string, boolean)}
         * calls. If the configured template loader is 
         * {@link StatefulTemplateLoader stateful}, then its 
         * {@link StatefulTemplateLoader#resetState()} method is invoked as well.
         */
        public void Clear()
        {
            lock (_storage)
            {
                _storage.Clear();
                if (_mainLoader is IStatefulTemplateLoader)
                {
                    ((IStatefulTemplateLoader) _mainLoader).ResetState();
                }
            }
        }

        public static string GetFullTemplatePath(Environment env, string parentTemplateDir, string templateNameString)
        {
            if (!env.IsClassicCompatible())
            {
                if (templateNameString.IndexOf("://") > 0)
                {
                    ;
                }
                else if (templateNameString.Length > 0 && templateNameString[0] == '/')
                {
                    int protIndex = parentTemplateDir.IndexOf("://");
                    if (protIndex > 0)
                    {
                        templateNameString = parentTemplateDir.Substring(0, protIndex + 2) + templateNameString;
                    }
                    else
                    {
                        templateNameString = templateNameString.Substring(1);
                    }
                }
                else
                {
                    templateNameString = parentTemplateDir + templateNameString;
                }
            }
            return templateNameString;
        }

        private object FindTemplateSource(string name, CultureInfo locale)
        {
            if (_localizedLookup)
            {
                int lastDot = name.LastIndexOf('.');
                string prefix = lastDot == -1 ? name : name.Substring(0, lastDot);
                string suffix = lastDot == -1 ? "" : name.Substring(lastDot);
                string localeName = LocaleSeparator + locale.Name;
                var buf = new StringBuilder(name.Length + localeName.Length);
                buf.Append(prefix);
                for (; ; )
                {
                    buf.Length = prefix.Length;
                    string path = buf.Append(localeName).Append(suffix).ToString();
                    object templateSource = AcquireTemplateSource(path);
                    if (templateSource != null)
                    {
                        return templateSource;
                    }
                    int lastUnderscore = localeName.LastIndexOf(LocaleSeparator);
                    if (lastUnderscore == -1)
                        break;
                    localeName = localeName.Substring(0, lastUnderscore);
                }
                return null;
            }
            else
            {
                return AcquireTemplateSource(name);
            }
        }

        private object AcquireTemplateSource(string path)
        {
            int asterisk = path.IndexOf(Asterisk);
            // Shortcut in case there is no acquisition
            if(asterisk == -1)
            {
                return _mainLoader.FindTemplateSource(path);
            }
            string[] tok = path.Split('/');
            int lastAsterisk = -1;
            var tokpath = new List<string>();
            foreach (string pathToken in tok)
            {
                if(pathToken.Equals(Asteriskstr))
                {
                    if(lastAsterisk != -1)
                    {
                        tokpath.RemoveAt(lastAsterisk);
                    }
                    lastAsterisk = tokpath.Count;
                }
                tokpath.Add(pathToken);
            }
            string basePath = ConcatPath(tokpath, 0, lastAsterisk);
            string resourcePath = ConcatPath(tokpath, lastAsterisk + 1, tokpath.Count);
            if(resourcePath.EndsWith("/"))
            {
                resourcePath = resourcePath.Substring(0, resourcePath.Length - 1);
            }
            StringBuilder buf = new StringBuilder(path.Length).Append(basePath);
            int l = basePath.Length;
            bool debug = Logger.IsDebugEnabled();
            for(;;)
            {
                string fullPath = buf.Append(resourcePath).ToString();
                if(debug)
                {
                    Logger.Debug("Trying to find template source " + fullPath);
                }
                object templateSource = _mainLoader.FindTemplateSource(fullPath);
                if(templateSource != null)
                {
                    return templateSource;
                }
                if(l == 0)
                {
                    return null;
                }
                l = basePath.LastIndexOf(Slash, l - 2) + 1;
                buf.Length = l;
            }
        }

        private static string ConcatPath(IList<string> path, int from, int to)
        {
            var buf = new StringBuilder((to - from)*16);
            for (int i = from; i < to; ++i)
            {
                buf.Append(path[i]).Append('/');
            }
            return buf.ToString();
        }

        private static string NormalizeName(string name)
        {
            if (name.IndexOf("://") > 0)
            {
                return name;
            }
            for (;;)
            {
                int parentDirPathLoc = name.IndexOf(ParentDirPath);
                if (parentDirPathLoc == 0)
                {
                    // If it starts with /../, then it reaches outside the template
                    // root.
                    return null;
                }
                if (parentDirPathLoc == -1)
                {
                    if (name.StartsWith(ParentDirPathPrefix))
                    {
                        // Another attempt to reach out of template root.
                        return null;
                    }
                    break;
                }
                int previousSlashLoc = name.LastIndexOf(Slash, parentDirPathLoc - 1);
                name = name.Substring(0, previousSlashLoc + 1) +
                       name.Substring(parentDirPathLoc + ParentDirPath.Length);
            }
            for (;;)
            {
                int currentDirPathLoc = name.IndexOf(CurrentDirPath);
                if (currentDirPathLoc == -1)
                {
                    if (name.StartsWith(CurrentDirPathPrefix))
                    {
                        name = name.Substring(CurrentDirPathPrefix.Length);
                    }
                    break;
                }
                name = name.Substring(0, currentDirPathLoc) +
                       name.Substring(currentDirPathLoc + CurrentDirPath.Length - 1);
            }
            // Editing can leave us with a leading slash; strip it.
            if (name.Length > 1 && name[0] == Slash)
            {
                name = name.Substring(1);
            }
            return name;
        }

        /// <summary>
        /// This class holds a (name, locale) pair and is used as the key in
        /// the cached templates map.
        /// </summary>
        private sealed class TemplateKey
        {
            internal readonly string Name;
            internal readonly CultureInfo Locale;
            internal readonly Encoding Encoding;
            internal readonly bool Parse;

            internal TemplateKey(string name, CultureInfo locale, Encoding encoding, bool parse)
            {
                Name = name;
                Locale = locale;
                Encoding = encoding;
                Parse = parse;
            }

            public override bool Equals(object o)
            {
                if (o is TemplateKey)
                {
                    var tk = (TemplateKey) o;
                    return
                        Parse.Equals(tk.Parse) &&
                        Name.Equals(tk.Name) &&
                        Locale.Equals(tk.Locale) &&
                        Encoding.Equals(tk.Encoding);
                }
                return false;
            }

            public override int GetHashCode()
            {
                return
                    Name.GetHashCode() ^
                    Locale.GetHashCode() ^
                    Encoding.GetHashCode() ^
                    (Parse ? false : true).GetHashCode();
            }
        }

        /**
         * This class holds the cached template and associated information
         * (the source object, and the last-checked and last-modified timestamps).
         * It is used as the value in the cached templates map. Note: this class
         * is Serializable to allow custom 3rd party CacheStorage implementations 
         * to serialize/replicate them (see tracker issue #1926150); FreeMarker 
         * code itself doesn't rely on its serializability.
         */
        private class CachedTemplate //: Cloneable, Serializable
        {
            private static readonly long serialVersionUID = 1L;

            internal object TemplateOrException;
            internal object Source;
            internal long LastChecked;
            internal long LastModified;

            public CachedTemplate CloneCachedTemplate()
            {
                //try
                //{
                //    return (CachedTemplate) base.Clone();
                //}
                //catch (CloneNotSupportedException e)
                //{
                //    throw new UndeclaredThrowableException(e);
                //}

                throw new NotImplementedException();
            }
        }
    }
}